package net.neocat.tcx.util;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import net.neocat.tcx.file.CoursePoint;
import net.neocat.tcx.file.GeoPosition;
import net.neocat.tcx.file.TrackPoint;

import java.util.Calendar;
import java.util.List;
import java.util.Map;

/**
 * Created with IntelliJ IDEA.
 * User: jschek
 * Date: 4/26/13
 * Time: 9:05 PM
 * To change this template use File | Settings | File Templates.
 */
public abstract class GeoUtils {

    private static final long epoch;

    static {
        Calendar calendar = Calendar.getInstance();
        calendar.set(2013, 01, 01, 00, 00, 00);
        epoch = calendar.getTimeInMillis();
    }

    public static List<CoursePoint> moveCoursePointsBack_Interpolate(double warningDistance, List<CoursePoint> coursePoints, List<TrackPoint> trackPoints) {
        List<CoursePoint> result = Lists.newArrayListWithExpectedSize(coursePoints.size());
        int lastTrackIndex = 0;
        for (CoursePoint point : coursePoints) {
            GeoPosition current = point.getPosition();
            int trackIndex = indexOf(current, trackPoints, lastTrackIndex);
            if (trackIndex == -1 || trackIndex == 0) {
                System.err.println("Could not find track point for course point " + point);
                result.add(point);
            } else {
                double distanceMoved = 0;
                GeoPosition newPosition = null;
                TrackPoint previousTrackPoint = null;
                int vertexesMoved = 0;
                while (distanceMoved < warningDistance && trackIndex > lastTrackIndex) {
                    trackIndex--;
                    previousTrackPoint = trackPoints.get(trackIndex);
                    newPosition = findIntermediatePoint(previousTrackPoint.getPosition(), current, warningDistance);
                    distanceMoved = current.greatCircleDistanceMeters2D(newPosition);
                    vertexesMoved++;
                }
                boolean newPointAdded = false;
                if (!newPosition.equals(previousTrackPoint.getPosition()) && !newPosition.equals(current)) {
                    trackPoints.add(trackIndex+1, new TrackPoint(previousTrackPoint.getTimestamp(), newPosition, newPosition.getElevationMeters(), previousTrackPoint.getDistanceMeters()+distanceMoved));
                    newPointAdded = true;
                }

                lastTrackIndex = trackIndex+1;
                System.out.println("Course moved " + Math.round(distanceMoved) + " meters. Vertexes=" + vertexesMoved + "; newpoint=" + newPointAdded);
                result.add(new CoursePoint(point.getName(), point.getTimestamp(), previousTrackPoint,
                        newPosition, point.getPointType(), point.getNotes()));
            }
        }
        return result;
    }

    public static List<CoursePoint> moveCoursePointsBack(double warningDistance, List<CoursePoint> coursePoints, List<TrackPoint> trackPoints) {
        List<CoursePoint> result = Lists.newArrayListWithExpectedSize(coursePoints.size());
        int lastTrackIndex = 0;
        for (CoursePoint point : coursePoints) {
            GeoPosition current = point.getPosition();
            int trackIndex = indexOf(current, trackPoints, lastTrackIndex);
            if (trackIndex == -1 || trackIndex == 0) {
                System.err.println("Could not find track point for course point " + point);
                result.add(point);
            } else {
                GeoPosition newPosition = null;
                TrackPoint previousTrackPoint = previousTrackPoint = trackPoints.get(trackIndex - 1);
                double distanceMoved = current.greatCircleDistanceMeters2D(previousTrackPoint.getPosition());
                if (distanceMoved <= warningDistance) {
                    newPosition = previousTrackPoint.getPosition();
                } else {
                    newPosition = current;
                    distanceMoved = 0;
                }
                lastTrackIndex = trackIndex;
                System.out.println("Course moved " + Math.round(distanceMoved) + " meters.");
                result.add(new CoursePoint(point.getName(), point.getTimestamp(), previousTrackPoint,
                        newPosition, point.getPointType(), point.getNotes()));
            }
        }
        return result;
    }

    private static GeoPosition findIntermediatePoint(GeoPosition source, GeoPosition destination, double targetDistance) {
        double distance = source.greatCircleDistanceMeters2D(destination);
        if (distance < targetDistance) {
            return source;
        }

        double percent = (distance - targetDistance) / distance;
        double xOffset = (destination.getLongitude() - source.getLongitude()) * percent;
        double yOffset = (destination.getLatitude() - source.getLatitude()) * percent;
        double zOffset = (destination.getElevationMeters() - source.getElevationMeters()) * percent;
        return source.plus(xOffset, yOffset, zOffset);
    }

    private static int indexOf(GeoPosition position, List<TrackPoint> trackpoints, int startIndex) {
        for (int i = startIndex; i < trackpoints.size(); i++) {
            if (trackpoints.get(i).getPosition().equals(position)) {
                return i;
            }
        }
        return -1;
    }

    public static List<GeoPosition> toPositions(List<TrackPoint> trackPoints) {
        List<GeoPosition> positions = Lists.newArrayListWithCapacity(trackPoints.size());
        for (TrackPoint trackPoint : trackPoints) {
            positions.add(trackPoint.getPosition());
        }
        return positions;
    }

    public static double approximateDegrees(double meters) {
        return meters / 111045.0;
    }

    public static double approximateDegrees(GeoPosition geoPosition, double meters) {
        double metersInDegree = approximateMeters(geoPosition,1.0);
        return meters/metersInDegree;
    }

    public static double approximateMeters(GeoPosition geoPosition, double degrees) {
        double dx = Math.sqrt(degrees*degrees/2.0);
        return geoPosition.greatCircleDistanceMeters2D(geoPosition.plus(dx,dx,0));
    }

    public static double approximateMeters(double degrees) {
        return 111045 * degrees;
    }

    public static Map<GeoPosition, CoursePoint> toMap(Iterable<CoursePoint> coursePoints) {
        Map<GeoPosition, CoursePoint> map = Maps.newLinkedHashMap();
        for (CoursePoint point : coursePoints) {
            map.put(point.getPosition(), point);
        }
        return map;
    }

    public static Map<GeoPosition, Double> toElevationMap(Iterable<TrackPoint> trackpoints) {
        Map<GeoPosition, Double> map = Maps.newHashMap();
        for (TrackPoint trackpoint : trackpoints) {
            map.put(trackpoint.getPosition(), trackpoint.getPosition().getElevationMeters());
        }
        return map;
    }
}
